#!/usr/bin/python3
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*-

import os
import sqlite3
from ..pdc_util import PdcUtil
from ..pdc_util import BluetoothAdvertisementMonitor
from ..pdc_param import PdcConst
from ..pdc_plugin_manager import PdcPluginBase


class Plugin(PdcPluginBase):

    def get_properties(self):
        return {
            "need-data-dir": True,
        }

    def start(self, context):
        self._componentList = []
        for c in context.machine.get_components():
            if c.get_vendor() == "berrymed" and c.get_model() == "BM1000":
                self._componentList.append(c)

        # no related device found
        if len(self._componentList) == 0:
            return

        self._dbConn = self._createDbConn(context)

        self._receiverDict = dict()

        uuidList = [c.get_id() for c in self._componentList]
        self._monitor = BluetoothAdvertisementMonitor(PdcConst.programName, self.name, self._deviceFound, uuidList)

    def stop(self):
        if hasattr(self, "_monitor"):
            self._monitor.dispose()
            del self._monitor
        if hasattr(self, "_receiverDict"):
            for obj in self._receiverDict.values():
                obj.dispose()
        if hasattr(self, "_dbConn"):
            self._dbConn.close()
            del self._dbConn

    def _deviceFound(self, device):
        self._receiverDict[device] = BluetoothDataReader(device, self._dataNotify)

    def _dataNotify(self, device, data):
        pass

    def _createDbConn(self, context):
        fullfn = os.path.join(context.get_data_dir(), "data.db")
        if os.path.exists(fullfn):
            conn = sqlite3.connect(fullfn)
            if PdcUtil.sqliteCheckTables(conn, self._getCreateTableSql()):
                return conn
            else:
                conn.close()
                PdcUtil.forceDelete(fullfn)

        conn = sqlite3.connect(fullfn)
        conn.executescript(self._getCreateTableSql())
        return conn

    @staticmethod
    def _getCreateTableSql():
        return '''
            CREATE TABLE session (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                device_uuid TEXT NOT NULL,
                start_timestamp INTEGER NOT NULL,
            );
            CREATE TABLE data (
                session_id INTEGER NOT NULL,
                timestamp INTEGER NOT NULL,             -- as unix epoch, unit: millisecond
                pleth INTEGER NOT NULL,
                bargraph INTEGER NOT NULL,
                spo2 INTEGER NOT NULL,                  -- blood oxygen saturation percentage, 0-100
                pulse_rate INTEGER NOT NULL,            -- heart rate in beats per minute
                signal_strength INTEGER NOT NULL,       -- bluetooth signal strength, 0-7
                PRIMARY KEY (session_id, timestamp),
                FOREIGN KEY (session_id) REFERENCES session(id) ON DELETE CASCADE
            );
        '''




class BluetoothDataReader:

    def __init__(self, device, dataNotifyCallback):
        self._device = device
        self._dataNotifyCallback = dataNotifyCallback

        # connect to device
        try:
            device_obj = self.bus.get_object('org.bluez', self.device_path)
            device_iface = dbus.Interface(device_obj, 'org.bluez.Device1')
            device_iface.Connect()
            
            print("Connected to BerryMed device")
            
            # Wait for services to resolve
            GLib.timeout_add(3000, self.setup_heart_rate_monitoring)
            
        except dbus.exceptions.DBusException as e:
            print(f"Connection failed: {e}")


        await self._client.connect()
        self._is_connected = True
        print('Successfully connected to device.')



        self._myPath = os.path.join("/", prefix, dbusPath)
        self._deviceFoundCallback = deviceFoundCallback

        self._monitorManager = None
        self._bMonitorCreated = False

        bus = dbus.SystemBus()
        bus.register_object(self._myPath, self, None)
        try:
            self._monitorManager = dbus.Interface(bus.get_object("org.bluez", "/org/bluez/hci0"), "org.bluez.LEAdvertisingMonitorManager1")
            self._monitorManager.CreateMonitor(self._myPath, {
                "Type": "passive",
                # "RSSIThreshold": -70,  # Optional: filter by signal strength
                # "UUIDs": ["0000180f-0000-1000-8000-00805f9b34fb"],  # Optional: filter by UUIDs
            })
            self._bMonitorCreated = True
        except BaseException:
            self.dispose()
            raise

    def dispose(self):
        bus = dbus.SystemBus()
        if self._bMonitorCreated:
            self._monitorManager.DestroyMonitor(self._myPath)
        bus.unregister_object(self._myPath)

    @dbus.service.method("org.bluez.LEAdvertisingMonitor1", in_signature='a{sv}', out_signature='')
    def DeviceFound(self, properties):
        self._deviceFoundCallback()

        # address = properties.get("Address", "Unknown")
        # rssi = properties.get("RSSI", "Unknown")
        # name = properties.get("Name", "Unknown")
        # print(f"Device found:")
        # print(f"  Address: {address}")
        # print(f"  Name: {name}")
        # print(f"  RSSI: {rssi} dBm")
        # print("------------------------")

    @dbus.service.method("org.bluez.LEAdvertisingMonitor1", in_signature='a{sv}', out_signature='')
    def DeviceUpdated(self, properties):
        address = properties.get("Address", "Unknown")
        print(f"Device updated: {address}")

    @dbus.service.method("org.bluez.LEAdvertisingMonitor1", in_signature='s', out_signature='')
    def DeviceLost(self, address):
        print(f"Device lost: {address}")



#!/usr/bin/python3

import os
import sys
import asyncio
import signal
import itertools
import threading
import time
from collections import namedtuple
from construct import BitStruct, BitsInteger, Flag, Bit, ConstructError
from datetime import datetime
from bleak import BleakClient, BleakScanner
from bleak.exc import BleakError

# UUIDs
DATA_SERVICE_UUID = '49535343-fe7d-4ae5-8fa9-9fafd205e455'
RECEIVE_CHARACTERISTIC = '49535343-1e4d-4bd9-ba61-23c647249616'
RENAME_CHARACTERISTIC_UUID = '00005343-0000-1000-8000-00805f9b34fb'

DATA_READ_TIMEOUT_MS = 20

# Bit parsing schema (same as original)
PARSING_SCHEMA = {
    0: BitStruct(sync_bit=Flag, pulse_beep=Flag, probe_unplugged=Flag, has_signal=Flag, signal_strength=BitsInteger(4)),
    1: BitStruct(sync_bit=Flag, pleth=BitsInteger(7)),
    2: BitStruct(sync_bit=Flag, pr_last_bit=Bit, pulse_research=Flag, no_finger=Flag, bargraph=BitsInteger(4)),
    3: BitStruct(sync_bit=Flag, pr_bits=BitsInteger(7)),
    4: BitStruct(sync_bit=Flag, spo2=BitsInteger(7))
}

HeartRateData = namedtuple('HeartRateData', ['signal_strength', 'has_signal', 'pleth', 'bargraph', 'no_finger',
                                             'pulse_rate', 'spo2', 'timestamp'])

def starting_bit_lookup_condition(x):
    try:
        return PARSING_SCHEMA[0].parse(x.to_bytes(1, 'big')).sync_bit
    except ConstructError:
        return False


def chunks(iterable, condition):
    """Yields split chunks for iterable containing sequences based on a passed condition for a head element."""
    it1, it2 = itertools.tee(iter(iterable))
    sequence_started = False
    while True:
        sequence_ended = False
        split_index = 1 if sequence_started else 0
        try:
            while not sequence_ended:
                item = next(it2)
                condition_result = condition(item)
                if condition_result and split_index == 0:
                    sequence_started = True
                elif condition_result and split_index != 0:
                    sequence_ended = True
                    break
                split_index += 1
        except StopIteration:
            pass

        if split_index and sequence_ended:
            sequence_started = True
        chunk = bytearray(itertools.islice(it1, split_index))
        if not chunk:
            return
        yield chunk

def device_filter(device, advertisement_data):
    return DATA_SERVICE_UUID in advertisement_data.service_uuids


class BluetoothPulseSensorReader:

    def __init__(self, handle_data_callback):
        print('Initializing...')
        print(f"Current time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        self._handle_data_callback = handle_data_callback
        self._keep_reading = True
        self._client = None
        self._is_connected = False

    # def _signal_handler(self, signum, frame):
    #     print(f'Received signal {signum}, terminating heartrate reader.')
    #     self._keep_reading = False
    #     asyncio.create_task(self._disconnect())

    async def _disconnect(self):
        if self._client and self._is_connected:
            await self._client.stop_notify(RECEIVE_CHARACTERISTIC)
            await self._client.disconnect()
            self._is_connected = False
            print('Disconnected from device')

    async def _connect(self):
        print('Searching for device...')
        
        try:
            # Scan for device using filter
            device = await BleakScanner.find_device_by_filter(
                device_filter,
                timeout=30.0
            )
            
            if not device:
                raise RuntimeError(f"Could not find device with characteristic {RECEIVE_CHARACTERISTIC}")            

            print(f'Found device: {device}')
            print(f'Device name: {device.name}')
            print(f'Device address: {device.address}')
            
            # Connect to device
            self._client = BleakClient(device)
            await self._client.connect()
            self._is_connected = True
            print('Successfully connected to device.')

            # Discover services (updated for modern Bleak)
            services = self._client.services
            print("Discovered services:")
            for service in services:
                print(f"  Service: {service.uuid} - {service.description}")
                for char in service.characteristics:
                    print(f"    Characteristic: {char.uuid} - {char.description}")
                    if char.properties:
                        print(f"      Properties: {', '.join(char.properties)}")

            return True
            
        except BleakError as e:
            print(f"Bluetooth connection error: {e}")
            raise RuntimeError(f"Failed to connect to device: {e}")

    def _notification_handler(self, sender: str, data: bytearray):
        """Handle incoming data from BLE characteristic notifications."""
        if not data:
            return

        try:
            for packet in chunks(data, starting_bit_lookup_condition):
                if len(packet) != len(PARSING_SCHEMA):
                    # Discarding broken packet
                    continue
                    
                packet_dict = {}
                try:
                    byte_1_data_container = PARSING_SCHEMA[0].parse(packet[0].to_bytes(1, 'big'))
                    byte_3_data_container = PARSING_SCHEMA[2].parse(packet[2].to_bytes(1, 'big'))
                    packet_dict['signal_strength'] = byte_1_data_container['signal_strength']
                    packet_dict['has_signal'] = byte_1_data_container['has_signal']
                    packet_dict['bargraph'] = byte_3_data_container['bargraph']
                    packet_dict['no_finger'] = byte_3_data_container['no_finger']
                    packet_dict['spo2'] = packet[4]
                    packet_dict['pleth'] = packet[1]
                    packet_dict['pulse_rate'] = packet[3] | ((packet[2] & 0x40) << 1)
                    packet_dict['timestamp'] = str(datetime.now())
                    
                    heart_rate_data = HeartRateData(**packet_dict)
                    self._handle_data_callback(heart_rate_data)
                    
                except Exception as e:
                    print(f'Error parsing packet data: {e}')
                    continue
                    
        except Exception as e:
            print(f'Error processing data: {e}')

    async def start_reading(self):
        """Main method to start reading heart rate data."""
        try:
            await self._connect()
            
            # Start notifications
            await self._client.start_notify(
                RECEIVE_CHARACTERISTIC, 
                self._notification_handler
            )
            
            # Keep the connection alive and reading
            while self._keep_reading:
                await asyncio.sleep(DATA_READ_TIMEOUT_MS / 1000)
                
        except Exception as e:
            print(f"Error in main reading loop: {e}")
        finally:
            await self._disconnect()

def handle_data(heart_rate):
    """Callback function to handle incoming heart rate data."""
    global c

    if c % 100 == 0:
        print(heart_rate)
    c += 1

    if heart_rate.no_finger:
        return

    with open("./rox.txt", "a") as f:
        f.write(str(heart_rate))
        f.write("\n")


def monitor_counter():
    """Monitor the counter and restart process if it doesn't change for 1 minute"""
    global c, reader

    while True:
        last_c = c
        time.sleep(10)
        if c != 0 and c == last_c:
            print(f"No data received for 1 minute (counter stuck at {c}), restarting process...")
            reader._keep_reading = False
            break


async def main():
    global c, reader

    c = 0
    
    # Start monitoring thread
    monitor_thread = threading.Thread(target=monitor_counter, daemon=True)
    monitor_thread.start()
    
    reader = BluetoothPulseSensorReader(handle_data)
    try:
        await reader.start_reading()
    except KeyboardInterrupt:
        print("Application terminated by user")
    except Exception as e:
        print(f"Application error: {e}")
    finally:
        await reader._disconnect()

if __name__ == "__main__":
    # Run the main async function
    asyncio.run(main())
